/*
 * Unidata Platform
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 *
 * Commercial License
 * This version of Unidata Platform is licensed commercially and is the appropriate option for the vast majority of use cases.
 *
 * Please see the Unidata Licensing page at: https://unidata-platform.com/license/
 * For clarification or additional options, please contact: info@unidata-platform.com
 * -------
 * Disclaimer:
 * -------
 * THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES, CONDITIONS AND
 * REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
 * IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY,
 * FITNESS FOR A PARTICULAR PURPOSE, DURABILITY, NON-INFRINGEMENT, PERFORMANCE AND
 * THOSE ARISING BY STATUTE OR FROM CUSTOM OR USAGE OF TRADE OR COURSE OF DEALING.
 */
package org.unidata.mdm.rest.meta.service;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;

import org.apache.commons.collections4.MapUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.type.model.AttributeElement;
import org.unidata.mdm.core.type.model.EntityElement;
import org.unidata.mdm.core.type.model.LookupElement;
import org.unidata.mdm.core.type.model.RegisterElement;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.meta.context.GetDataModelContext;
import org.unidata.mdm.meta.context.GetDataModelContext.GetDataModelContextBuilder;
import org.unidata.mdm.meta.context.UpsertDataModelContext;
import org.unidata.mdm.meta.context.UpsertDataModelContext.UpsertDataModelContextBuilder;
import org.unidata.mdm.meta.dto.GetEntityDTO;
import org.unidata.mdm.meta.dto.GetModelDTO;
import org.unidata.mdm.rest.meta.converter.EntityDefinitionConverter;
import org.unidata.mdm.rest.meta.converter.NamedDisplayableConverter;
import org.unidata.mdm.rest.meta.exception.MetaRestExceptionIds;
import org.unidata.mdm.rest.meta.ro.DeleteModelEntityRO;
import org.unidata.mdm.rest.meta.ro.EntityRO;
import org.unidata.mdm.rest.meta.ro.GetModelRO;
import org.unidata.mdm.rest.meta.ro.ReferenceInfo;
import org.unidata.mdm.rest.meta.ro.references.EntityReferenceRO;
import org.unidata.mdm.rest.meta.ro.references.LookupReferenceRO;
import org.unidata.mdm.rest.meta.type.rendering.MetaModelInputRenderingAction;
import org.unidata.mdm.rest.meta.type.rendering.MetaModelOutputRenderingAction;
import org.unidata.mdm.rest.system.ro.ErrorResponse;
import org.unidata.mdm.rest.system.ro.RestResponse;
import org.unidata.mdm.system.exception.PlatformBusinessException;
import org.unidata.mdm.system.service.RenderingService;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;

/**
 * @author Michael Yashin. Created on 19.05.2015.
 */
@Path(EntityRestService.SERVICE_PATH)
@Consumes({"application/json"})
@Produces({"application/json"})
public class EntityRestService extends AbstractFilteringRestService {
    /**
     * Service path.
     */
    public static final String SERVICE_PATH = "entities";
    /**
     * The RS.
     */
    @Autowired
    private RenderingService renderingService;
    /**
     * Meta model service.
     */
    @Autowired
    private MetaModelService metaModelService;
    /**
     * Gets entities in paged fashion with defaults.
     * @param pageRequest the page request
     * @return list of entities
     */
    @GET
    @Operation(
        description = "List of registers.",
        method = HttpMethod.GET,
        parameters = @Parameter(name = "draftId", description = "Draft id. Optional.", in = ParameterIn.QUERY),
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response findAll(@QueryParam("draftId") @DefaultValue("0") long draftId) {

        GetDataModelContext ctx = GetDataModelContext.builder()
                .allEntities(true)
                .reduced(true)
                .draftId(draftId > 0 ? draftId : null)
                .build();

        GetModelDTO dto =  metaModelService.get(ctx);
        return ok(NamedDisplayableConverter.to(dto.getEntities().stream()
                .map(GetEntityDTO::getEntity)
                .filter(el -> allow(el.getName()))
                .collect(Collectors.toList())));
    }
    /**
     * Gets an entity by id.
     * @param id entity id
     * @return entity
     * @throws Exception if something went wrong
     */
    @GET
    @Path("{id}")
    @Operation(
        description = "Gets register by ID.",
        method = HttpMethod.GET,
        parameters = {
                @Parameter(name = "id", description = "The ID", in = ParameterIn.PATH),
                @Parameter(name = "draftId", description = "Exisitng draft ID. Optional.", in = ParameterIn.QUERY),
                @Parameter(name = "checkData", description = "Set hasData flag or not", in = ParameterIn.QUERY)
        },
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response getById(
             @PathParam("id") String id,
             @QueryParam("draftId") @DefaultValue("0") long draftId,
             @QueryParam("checkData") @DefaultValue("true") boolean checkData) {

        if (!allow(id)) {
            return notAuthorized(id);
        }

        GetModelRO ro = new GetModelRO();

        ro.setCheckData(checkData);
        ro.setDraft(draftId > 0);
        ro.setDraftId(draftId);
        ro.setId(id);

        GetDataModelContextBuilder requestContext = GetDataModelContext.builder()
            .entityIds(Collections.singletonList(id))
            .draftId(draftId > 0 ? draftId : null);

        renderingService.renderInput(MetaModelInputRenderingAction.GET_ENTITY_MODEL_INPUT, requestContext, ro);

        GetModelDTO model = metaModelService.get(requestContext.build());

		throwIfNotFound(model);

        EntityRO response = EntityDefinitionConverter.to(model.getEntities().get(0));

        renderingService.renderOutput(MetaModelOutputRenderingAction.GET_ENTITY_MODEL_OUTPUT, model, response);

        return ok(new RestResponse<>(response));
    }

    /**
     * Creates a new entity.
     * @param entity new entity
     * @return the entity just created
     * @throws Exception if something went wrong
     */
    @POST
    @Operation(
        description = "Create register or lookup.",
        method = HttpMethod.POST,
        parameters = @Parameter(name = "draftId", description = "Exisitng draft ID. Optional.", in = ParameterIn.QUERY),
        requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = EntityRO.class)), description = "Entity request body."),
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
	public Response create(EntityRO entity, @QueryParam("draftId") @DefaultValue("0") long draftId) {

        if (!allow(entity.getName())) {
            return notAuthorized(entity.getName());
        }

        UpsertDataModelContextBuilder uCtx = EntityDefinitionConverter.from(entity, draftId);

        renderingService.renderInput(MetaModelInputRenderingAction.UPSERT_ENTITY_MODEL_INPUT, uCtx, entity);

        metaModelService.upsert(uCtx.build());

        GetModelRO ro = new GetModelRO();
        ro.setCheckData(true);
        ro.setDraftId(draftId);
        ro.setId(entity.getName());

        GetDataModelContextBuilder gCtx = GetDataModelContext.builder()
                .entityIds(Collections.singletonList(entity.getName()))
                .draftId(draftId > 0 ? draftId : null);

        renderingService.renderInput(MetaModelInputRenderingAction.GET_ENTITY_MODEL_INPUT, gCtx, ro);

        GetModelDTO model = metaModelService.get(gCtx.build());

        if (model.getEntities().isEmpty()) {
            throw new PlatformBusinessException("Entity not found", MetaRestExceptionIds.EX_META_DATA_ENTITY_NOT_FOUND);
        }

        EntityRO response = EntityDefinitionConverter.to(model.getEntities().get(0));

        renderingService.renderOutput(MetaModelOutputRenderingAction.GET_ENTITY_MODEL_OUTPUT, model, response);

        return ok(new RestResponse<>(response));
    }
    /**
     * Updates an entity definition.
     * @param entity the entity
     * @return updated entity
     * @throws Exception
     */
    @PUT
    @Path("{id}")
    @Operation(
        description = "Updates an entity or lookup definition. Complete set of properties is expected.",
        method = HttpMethod.PUT,
        parameters = {
                @Parameter(name = "id", description = "Идентификатор,", in = ParameterIn.PATH),
                @Parameter(name = "draftId", description = "Existing draft ID. Optional.", in = ParameterIn.QUERY, required = false)
        },
        requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = EntityRO.class)), description = "Entity request body."),
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response update(@PathParam("id") String possiblyOldName, @QueryParam("draftId") @DefaultValue("0") long draftId, EntityRO entity ) {

        if (!allow(entity.getName())) {
            return notAuthorized(entity.getName());
        }

        // upsert
        // 1. Convert ro object
        UpsertDataModelContextBuilder uCtx = EntityDefinitionConverter.from(entity, draftId);

        // 2. Run upsert execution
        renderingService.renderInput(MetaModelInputRenderingAction.UPSERT_ENTITY_MODEL_INPUT, uCtx, entity);
        metaModelService.upsert(uCtx.build());

        // 3.  Get new model after upsert
        GetModelRO ro = new GetModelRO();
        ro.setCheckData(true);
        ro.setDraftId(draftId);
        ro.setId(entity.getName());

        GetDataModelContextBuilder gCtx = GetDataModelContext.builder()
                .entityIds(Collections.singletonList(entity.getName()))
                .draftId(draftId > 0 ? draftId : 0);

        renderingService.renderInput(MetaModelInputRenderingAction.GET_ENTITY_MODEL_INPUT, gCtx, ro);

        GetModelDTO model = metaModelService.get(gCtx.build());

        throwIfNotFound(model);

        // 4. Convert and return output result
        EntityRO response = EntityDefinitionConverter.to(model.getEntities().get(0));
        renderingService.renderOutput(MetaModelOutputRenderingAction.GET_ENTITY_MODEL_OUTPUT, model, response);

        return ok(new RestResponse<>(response));
    }
    /**
     * Deletes an entity.
     * @param id entity id
     * @return empty string
     * @throws Exception
     */
    @DELETE
    @Path("{id}")
    @Operation(
        description = "Removes an entity or lookup definition.",
        method = HttpMethod.DELETE,
        parameters = {
                @Parameter(name = "id", description = "Entity or lookup id.", in = ParameterIn.PATH),
                @Parameter(name = "draftId", description = "Existing draft ID. Optional.", in = ParameterIn.QUERY)
        },
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = String.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response delete(
             @PathParam("id") String id,
             @QueryParam("draftId") @DefaultValue("0") long draftId) {

        if (!allow(id)) {
            return notAuthorized(id);
        }

        UpsertDataModelContextBuilder uCtx = UpsertDataModelContext.builder()
                .entitiesDelete(id)
                .draftId(draftId > 0 ? draftId : null);

        renderingService.renderInput(MetaModelInputRenderingAction.DELETE_ENTITY_META_MODEL_INPUT, uCtx, new DeleteModelEntityRO(id));

        metaModelService.upsert(uCtx.build());

        return ok("");
    }


    /**
     * Get all records with lookup link to sending lookup id
     * @param lookupId lookup id
     * @return records linked to lookup
     * @throws Exception if something went wrong
     */
    @GET
    @Path("lookup-links/{id}")
    @Operation(
        description = "Gets objects, having references to this lookup.",
        method = HttpMethod.GET,
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response getEntitiesReferencingThisLookup(
            @Parameter(description = "Lookup id.", in = ParameterIn.PATH) @PathParam("id") String lookupId,
            @Parameter(description = "Draft id. Optional.", in = ParameterIn.QUERY) @QueryParam("draft") @DefaultValue("false") boolean draft) {

        List<ReferenceInfo> result;
        if (draft) {
            result = Collections.emptyList();
        } else {

            EntityElement lookup = metaModelService.instance(Descriptors.DATA).getLookup(lookupId);
            if (Objects.isNull(lookup)) {
                return ok(new RestResponse<>(Collections.emptyList()));
            }

            Map<RegisterElement, Set<AttributeElement>> dependencies = lookup.getLookup().getReferencingRegisters();
            result = new ArrayList<>();
            if (MapUtils.isNotEmpty(dependencies)) {
                for (EntityElement entityDef : dependencies.keySet()) {
                    ReferenceInfo referenceInfo = new ReferenceInfo();
                    referenceInfo.setTargetKey(new LookupReferenceRO(lookupId));
                    referenceInfo.setSourceKey( new EntityReferenceRO(entityDef.getName()));
                    referenceInfo.setSourceType(referenceInfo.getSourceKey().keyType().getName());
                    referenceInfo.setTargetType(referenceInfo.getTargetKey().keyType().getName());
                    result.add(referenceInfo);
                }
            }

            Map<LookupElement, Set<AttributeElement>> lookupDependencies = lookup.getLookup().getReferencingLookups();
            if (MapUtils.isNotEmpty(lookupDependencies)) {
                for (EntityElement entityDef : lookupDependencies.keySet()) {
                    ReferenceInfo referenceInfo = new ReferenceInfo();
                    referenceInfo.setTargetKey(new LookupReferenceRO(lookupId));
                    referenceInfo.setSourceKey( new LookupReferenceRO(entityDef.getName()));
                    referenceInfo.setSourceType(referenceInfo.getSourceKey().keyType().getName());
                    referenceInfo.setTargetType(referenceInfo.getTargetKey().keyType().getName());
                    result.add(referenceInfo);
                }
            }
        }

        return ok(new RestResponse<>(result));
    }
    /**
     * Get all available tags for this entity.
     * @param id entity name
     * @param draft is draft
     * @return all available tags for thos entity.
     */
    @GET
    @Path(value = "/tags/{id}")
    @Operation(
        description = "Gets all tags for a register.",
        method = HttpMethod.GET,
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
	public Response getAllTags(
	        @Parameter(description = "Register id.", in = ParameterIn.PATH) @PathParam("id") String id,
            @Parameter(description = "Draft id. Optional.", in = ParameterIn.QUERY) @QueryParam("draftId") @DefaultValue("0") long draftId) {

        GetModelDTO model = metaModelService.get(GetDataModelContext.builder()
                    .entityIds(Collections.singletonList(id))
                    .draftId(draftId > 0 ? draftId : null)
                    .build());

        throwIfNotFound(model);

		return ok(new RestResponse<>(new HashSet<>()));
	}

    private void throwIfNotFound(GetModelDTO result) {
        if (result.getEntities().isEmpty()) {
            throw new PlatformBusinessException("Entity not found", MetaRestExceptionIds.EX_META_DATA_ENTITY_NOT_FOUND);
        }
    }
}